<?php

/* PhpWebStorage - Reliable file-based data storage for multi-threaded environments
 * Version 0.9.4
 * Copyright 2009-2010, Virthos Systems GmbH
 * http://code.google.com/phpwebstorage
 * ===================================================================
 *
 * DESCRIPTION
 *
 * PhpWebStorage is a light-weight PHP class for saving to and reading from
 * data files in a multi-threaded web server environment. It is an alternative
 * to using a full-sized database system, especially in cases where the 
 * amount of stored data is low, the available database system is slow
 * (like in some shared hosting environments) or the web application should
 * be easily installable and transportable.
 * 
 * One of the core features of PhpWebstorage is a locking
 * mechanism which prevents multiple processes to access the same file
 * at the same time. Writing to files is secured so that an interruption of
 * the writing process (i.e by user abort or server crash) does not corrupt
 * the previous stored data. All this is achieved without using any external
 * (non-standard) PHP libraries or system extensions so that the class may
 * be used on any web server under any PHP version starting with v4.1.
 *
 * The PhpWebStorage class automatically serializes any data before writing
 * it to the file. If the file lives in a public accessible directory the
 * class can prevent it from being downloaded by wrapping it as a pseudo
 * PHP script file which will produce only some errors and warnings when 
 * someone tries to download it. The optional base64-encoding prevents
 * corruption of the serialized data when transferring the data file across
 * transportation channels which modifies the line endings of text files.
 *
 *
 * USAGE
 *
 * // This is a simple example
 * require_once( 'phpwebstorage.class.php' );
 * $storage = new PhpWebStorage( $pathToDBFile );
 * $content = $storage->read( true );
 * register_shutdown_function( array( $storage, 'unlock' ) );
 * // do some modifications on $content here
 * $storage->write( $content );
 * $storage->unlock();
 * 
 * 
 * TODO
 * - Make return values more informative (error reporting)
 * - Make umask for newly created files configurable
 *
 *
 * LICENSE
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of the
 * License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA
 *
 */



/**
 * Class PhpWebStorage.
 * To use, create an instance of the class. Then use read() and write() to
 * store and retrieve data. 
 */

class PhpWebStorage {

	/**
	 * Field lockAll.
	 * If set to true locking occurs for the whole directory, not only the
	 * current data file. This is helpful when data is spread among multiple 
	 * files, and consistency of those files is critical.
	 *
	 * WARNING: Make sure you use the locking mechanism consistently, i.e.
	 * not mixing file and directory locking within the same directory.
	 * If $lockAll is enabled there is no check if a particular file is 
	 * locked individually and vice versa. It is impossible to implement
	 * such a double-check reliably in a multi-threaded environment. 
	 */
	var $lockAll = false;

	/**
	 * Field lockSuffix.
	 * When locking a file a directory of the same name, extended by this
	 * suffix, is created. When locking a whole directory this suffix is 
	 * used as the name of the lock directory.
	 */
	var $lockSuffix = '_lock';

	/**
	 * Field timeout.
	 * Number of seconds until the script gives up trying to access a 
	 * locked file.
	 */
	var $timeout = 10;

	/**
	 * Field autoUnlock.
	 * Sometimes a process may quit without unlocking, leaving some files or
	 * directories in a locked state. These files may be unlocked 
	 * automatically after the specified number of seconds. You can switch off
	 * this feature by setting $autoUnlock to 0 or false. In this case you 
	 * have to monitor (and repair) your web application manually.
	 */
	var $autoUnlock = 600;

	/**
	 * Field backup.
	 * Normally the current data file is backed up before replacing it by
	 * the new one. Set this flag to false if you do not need this extra
	 * security.
	 */
	var $backup = true;

	/**
	 * Private fields. 
	 * Do not touch!
	 */
	var $file = '';
	var $folder = '';
	var $lockFile = '';
	var $backupFile = '';
	var $tempFile = '';
	var $isLocked = false;
	var $storeSecure = true;
	

	/*** PUBLIC METHODS ***/
	

	/**
	 * Method PhpWebStorage. 
	 * Constructor function. See "USAGE" for details. 
	 * If the second parameter is set to true all data will be 
	 * base64 encoded and wrapped in a pseudo php script file whose 
	 * content cannot be read via http. You should stay with this 
	 * setting unless you really know that it is not neccessary for 
	 * your application and you want to save some CPU cycles.
	 */
	function PhpWebStorage( $path, $storeSecure = true ){

		$this->storeSecure = $storeSecure;

		if( $path ){

			$this->folder = dirname( $path );

			if( $this->storeSecure && substr( $path, -4 ) != '.php' ){
				$this->file = $path . '.php';
			} else {
				$this->file = $path;
			}

			if( $this->storeSecure ){
				$this->backupFile = substr( $this->file, 0, -4 ) . '_bak.php';
			} else {
				$this->backupFile = $this->file . '_bak';
			}

			$this->tempFile = $this->folder . '/~' . basename( $this->file );
			$this->lockFile = $this->file . '_lock';

		}
	}


	/**
	 * Method write. 
	 * Writes data to the storage file. $data may be any object which can be
	 * serialized (i.e. converted to a string) by the PHP engine.
	 * If the storage file has not been locked previously it is automatically 
	 * locked before the write process starts and unlocked afterwards.
	 */
	function write( $data ){
		
		if( !$this->folder ){
			return false;
		}
		
		if( $this->isLocked ){
			$hadBeenLocked = true;
		} else {
			$hadBeenLocked = false;
			if( !$this->lock() ){
				return false;
			}
		}

		$fp = fopen( $this->tempFile, 'w+' );
		if( !$fp ){
			return false;
		}

		if( $this->storeSecure ){
			$result = fwrite( $fp, '<?php ' . base64_encode( serialize( $data ) ) . ' ?>');
		} else {
			$result = fwrite( $fp, serialize( $data ) );
		}

		fclose( $fp );

		if( $result !== false ) {
			if( $this->backup ) {
				if( file_exists( $this->backupFile ) ) {
					unlink( $this->backupFile );
				}
				if( file_exists( $this->file ) ) {
					rename( $this->file, $this->backupFile );
				}
				rename( $this->tempFile, $this->file );
			} else {
				if( file_exists( $this->file ) ) {
					unlink( $this->file );
				}
				rename( $this->tempFile, $this->file );
			}
			$result = true;
		} else {
			$result = false;
		}
		
		if( !$hadBeenLocked ){
			$this->unlock();
		}
		
		return $result;
	}
	

	/**
	 * Method read. 
	 * Reads data from the storage file and returns it as an object/array.
	 */
	function read( $lock = false ){
		
		if( !$this->folder ){
			return false;
		}
		
		if( $lock ){
			if( !$this->lock() ){
				return false;
			}
		}
		
		if( file_exists( $this->file ) ){
			if( file_exists( $this->tempFile ) ){
				if( file_exists( $this->backupFile ) ){
					$fp = fopen( $this->backupFile, 'r' );
				} else {
					return false;
				}
			} else {
				$fp = fopen( $this->file, 'r' );
			}
		} else {
			return false;
		}

		if( !$fp ) {
			return false;
		}
		
		if( filesize( $this->file ) ){
			$content = fread( $fp, filesize( $this->file ) );
			if( substr( $content, 0, 5 ) == '<?php' ){
				$object = unserialize( base64_decode( substr( $content, 6, -3 ) ) );
			} else {
				$object = unserialize( $content );
			}
		} else {
			$object = array();
		}

		fclose( $fp );
		return $object;
	}


	/**
	 * Method lock. 
	 * Locks the storage file or the whole directory, depending on the
	 * $lockAll setting. Normally, you do not have to call this function
	 * explicitly as this can be done implicitly with the read() function.
	 */
	function lock(){

		if( $this->isLocked ){
			return true;
		}
		
		if( !file_exists( $this->folder ) ){
			return false;
		}

		if( $this->lockAll ){
			$path = $this->folder . '/' . $this->lockSuffix;
		} else {
			$path = $this->lockFile;
		}

		if( file_exists( $path ) && $this->autoUnlock ){
			$mtime = filemtime( $path );
			if( ( time() - $mtime ) > $this->autoUnlock ){
				@rmdir( $path );
			}
		}

		$oldUmask = umask(0);
		$connectTime = time();

		while( !@mkdir( $path, 0777 ) ) {
			if( time() - $connectTime > $this->timeout ) {
				return false;
			}
			usleep(100000);
		}
		
		umask( $oldUmask );
		$this->isLocked = true;

		return true;
	}

	/**
	 * Method unlock. 
	 * Unlocks the storage file or the whole directory, depending on the
	 * $lockAll setting. It is a good idea to define a shutdown function
	 * which does the unlocking in case of a user abort or timeout.
	 */

	function unlock(){	

		if( $this->lockAll ){
			$path = $this->folder . '/' . $this->lockSuffix;
		} else {
			$path = $this->lockFile;
		}

		@rmdir( $path );
		
		$this->isLocked = false;
	}
}
?>